# -*- coding: utf-8 -*- 

"""
This file is part of PeerTool, a web application for collaborative
decision-making (Copyrighted, 2009-2010, Ellis & Grant, Inc.)
Developed by Michael F. Ellis <michael.f.ellis@gmail.com>.  
License: AGPL v3
PeerTool comes with ABSOLUTELY NO WARRANTY.
"""                
import sys
import cPickle as pickle
import datetime
import os
import re
import time
import zlib
compress, decompress = zlib.compress, zlib.decompress  
#from gluon.contrib.markmin import render  as mm
#def xmm(s) : return XML(mm(s))
from gluon.storage import Storage
from gluon.contrib import simplejson

ptproblem = local_import('ptproblem',reload=True)

class LimitException(Exception):
    """ Used when we detect that a user has exceeded a limit. """
    def __init__(self, value):              # Report whodunnit.
        self.id = auth.user.id
        self.who = auth.user.email
        self.message = "Limit exceeded by "\
            "user %d (%s): %s"%(self.id,self.who,value)
        

    def __str__(self):
       return repr(self.message) 

class SizeLimitException(LimitException):
    pass

class InvitationLimitException(LimitException):
    pass

class ActivityLimitException(LimitException):
    pass

class ItemLimitException(LimitException):
    pass

## DB TABLES

#The problems table contains all problems created by users. The vast majority of
#the data is contained in blob field named 'obj' containing a compressed
#serialized python object of class Problem defined in module ptproblem.py.
#The other fields contain meta-information about the problem object including
#time of last modification, a label derived from the user's problem description,
#and various other status information.

db.define_table("problems",
    Field("timestamp","datetime"),          ## changes whenever the
                                            ## problem object is updated

    Field("label", 'string', length=100),   ## from 1st line of 
                                            ## problem description

    Field("revision", 'integer', 
        notnull=True, default=1),           ## increments when items change.
                                            ## Used by update() for transactional
                                            ## integrity

    Field("isembryo", "boolean", 
        notnull=True, default=True),        ## editable and no invites 
                                            ## released while this is True.

    Field("creatorid",'integer'),           ## needed so Choose page can
                                            ## allow creator to select
                                            ## problem before it gets
                                            ## delivered.

    Field("created","datetime"),            ## Timestamp for creation. Used 
                                            ## mostly for limit checking.

    Field("participants", 'integer'),       ## i.e. no. of accepted invites

    Field("solutions",'integer'),           ## how many solutions

    Field("criteria",'integer'),            ## how many criteria

    Field("size","integer"),                ## size of pickled problem object

    Field("gzsize","integer"),              ## gzipped size

    Field("isfrozen", 'boolean',            ## FUTURE -- allow for problems
            default=False),                 ## to refuse edits and responses
                                            ## as a way to support examples
                                            ## and archiving.

    Field("ispublic", 'boolean',            ## FUTURE -- allow problems to
            default=False),                 ## publicly accessible. Needs 
                                            ## code that will hide email
                                            ## email addresses and support
                                            ## text and/or topical searches.

    Field("description", 'text',            ## FUTURE -- Store a copy of  
            default=""),                    ## the current problem
                                            ## description to support a
                                            ## background indexing task.


    Field("obj", "blob"),                   ## The ptproblem problem object
                                            ## for this problem

    migrate=PTCFG.development)              ## i.e False for production
 

                
#Users can be team members on many problems.  The invitations table
#contains a row for each membership.  Problem participants specify the
#list of team members by email addresses, so a lookup is necessary to
#find user ids that match the email addresses.  Not all email addresses
#may find a match since the a participant might incorrectly type the
#email addresses or the team member may not be registered.  For the
#public version of this application,  the problem participants will not
#have access to the registered users list.  We do this to deter spamming.  

db.define_table("invitations",
    Field("problemid", db.problems),            ## id of the problem

    Field("email","string", length=100),        ## email of invitee

    Field("invitedby","string", length=100),    ## who issued the invitation

    Field("accepted","boolean",default=None),   ## User has accepted (TRUE), 
                                                ## declined (FALSE), 
                                                ## or no decision (NULL)

    migrate=True)                               ## set this False for production


# FUNCTIONS



def myXML(text,xtags=None, xattrs=None):
    """ Slightly looser than XML().  Allows embedding video objects """

    permitted = ['a', 'b', 'blockquote', 
        'br/', 'i', 'li', 'ol', 'ul', 'p', 
        'cite', 'code', 'pre', 'em', 'strong', 
        'img/','object','embed']

    allowed = {'a':['href', 'title'],
       'img':['src', 'alt'], 
       'blockquote':['type'], 
       'object':['height','width'], 
       'embed':['allowfullscreen','src','type']}

    if xtags: 
        permitted_tags.extend(
            [t for t in xtags 
                if t not in permitted_tags])
    if xattrs: 
        allowed_attributes.update(xattrs)

    return XML(text, sanitize=True,
        permitted_tags=permitted,
        allowed_attributes=allowed)

                
def JD(obj):
    """ 
    Serializes python objects to JSON notation. Used primarily in views to
    inject object declarations into javascript. Argument 'obj' can be any
    arbitrarily nested python dict or list that ultimately resolves to
    primitive elements, i.e. ints, strings, and floats.  
    """

    return XML(simplejson.dumps(obj,indent=4))


def getPT(extensions=None):
    """ 
    Defines the dictionary object, PT, that we push into javascript via
    simplejson.dumps() This function is typically called from a view and
    extensions may be supplied to provide information needed by
    javascript associated with the view. If supplied, extensions must be
    a dict that can be serialized by simplejson.dumps().
    """
    problemid = session.problemid
    #T.lazy = False
    PT = dict(
        pagehelp = "NEEDS TEXT!",
        ## URLs
        jsonurl = str(URL(
            r=request,c='results',
            f='call/json/serveresults/%(problemid)d'%locals())),
        jsoncommenturl = str(URL(
            r=request,c='results',
            f='call/json/servecomments/%(problemid)d'%locals())),
        
        ## strings that need translation
        T = dict(   
            comments = str(T('comments')),
            comment  = str(T('comment')),
            commenttip = str(T('Click to show/hide comments')),
            fulltexttip = str(T('Click to show/hide full text')),
            newitemicontip = str(T('Click to add new item')),
            olditemicontip = str(T('Click to edit this item')),
            responses   = str(T('Responses:')) ,
            votesfordeletion = str(T('Votes for deletion:')), 
            ),
        );
    if extensions:
        PT.update(extensions)
    #T.lazy = True
    return PT        

def plrl(n,singular,plural):
    """
    >>> plrl(2,'goose','geese')
    2 geese
    >>> plrl(1,'goose','geese')
    1 goose
    """
    return plural if abs(n) != 1 else singular

def formatcomment(t, c, withname=True):
    """ 
    Adds username and pretty date. 
    Called when outputting a comment 
    """
    assert isinstance(c, ptproblem.Comment)

    header = []
    if withname:
        header.append("<strong>%s</strong>"%c.who)
      
    dt = datetime.datetime.fromtimestamp(t) 
    header.append(str(prettydate(dt,T=T)))

    hstring = " ".join(header)
    return hstring + '<br/>' +c.text 

    
## ---------------------------------------------------------------
## Interface functions for creating, loading, and updating Problems.
## ----------------------------------------------------------------
def checkInvitationLimits():
    """ 
    Make sure user isn't trying to exceed limits on invitations. 
    Returns None or raises InvitationLimitException.
    """
    s = session.problemid
    i= db(db.invitations.id==s).count()
    if i > PTCFG.maxInvitations:
        raise InvitationLimitException("Too many invitations!")
    else:
        return None
            

def checkLimits(problem, new=False):
    """
    Called before committing problem to db. Tests against limits in PTCFG and
    raise LimitException on failure.  Otherwise returns a dict containing
    compressed serialization of the problem object for storage plus info from
    the problem that gets stored in separate fields in the problems table. Set
    new to True if the problem is about to be created as opposed to updated.
    """

    p = problem
    c = PTCFG
    u = auth.user.id
    s = session.problemid

    sltns = len(p.solutions)
    crtr  = len(p.criteria)
    if sltns*crtr > c.maxSxC:
        raise ItemLimitException("Too many solutions and criteria!")

    #if new:
    #    hr = datetime.datetime.fromtimestamp(time.time()-3600)
    #    qry = (db.problems.creatorid==u)&(db.problems.created > hr)  
    #    if db(qry).count() > c.maxCreationsPerHour:
    #        raise ActivityLimitException(
    #            "Too many problems created in last hour!")

    pckl = pickle.dumps(p)
    sz = len(pckl)
    gz = compress(pckl)
    gzsz = len(gz)
    if gzsz > c.maxGzProblem:
        raise SizeLimitException("Problem exceeded size limit!")

    return dict(gz=gz,sz=sz,gzsz=gzsz,
                sltns=sltns, crtr=crtr)

def createProblem(formvars):
    """
    Instantiates a new problem object, pickles it, and adds a record to the
    problems table in the db.
    """    
    debug("Creating problem %s"%formvars.description)  

    ## collect user's preferred synonyms
    d={}
    for i in PTCFG.synonyms:
        d[i] = formvars[i]

    ## The default solution
    s0 = "%s\n%s"%(P(str(T("Status Quo"))),P(str(T("It is often useful to have a "\
    "do nothing or none of the above alternative. Feel free to revise "\
    "or delete this item if it is not useful in the context of your"))+\
    " "+d['problem']+"."))

    ## The default criterion
    c0 = "%s\n%s"%(P(str(T("Everything else"))), P(str(T("You can use this as a "\
    "catch-all to avoid having to add a large number of minor considerations "\
    "to your list of"))+d['criteria']+'.')) 

    problem = ptproblem.Problem(formvars.description,
        creatorid=auth.user.id,
        defaultsolutiontext=s0,
        defaultcriteriontext=c0,)

    problem.setSynonyms(d)

    ## insert it into the database
    id = _addNewProblemToDb(problem)

    return id

def cloneProblem(oldproblem):
    """
    Instantiates a cloned problem object, pickles it, and adds a record to the
    problems table in the db.
    """    
    debug("Cloning problem %s"%oldproblem.text)   
    problem = ptproblem.cloneProblem(
        oldproblem,newcreatorid=auth.user.id)
    ## insert it into the database
    id = _addNewProblemToDb(problem)
    
    # Clone invitations
    rows = db(db.invitations.problemid == session.problemid).select()
    for r in rows:
        if r.email != auth.user.email:
            db.invitations.insert(email=r.email, 
                invitedby=auth.user.email,
                problemid=id, 
                )
    return id

def loadProblem(id):
    """ 
    Unpickles and returns a  problem.
    IMPORTANT! By design, this is the ONLY function that should be allowed to
    load a problem. Among other reasons, this makes it easy to restrict problem
    access to invited participants who have accepted the invitation.
    """
    debug("About to load problem")
    rows = db(db.problems.id==id).select()
    if len(rows) == 0:
        return None
    assert len(rows) == 1
    obj = rows[0]['obj']

    problem = pickle.loads(decompress(obj))

    ## Check if team member
    if not auth.user.id in problem.lastresponses:
        dbi = db.invitations
        qry =  (dbi.problemid==id)&(dbi.email==auth.user.email)
        irows = db(qry).select()
        if 0 == len(irows) or True != irows[0].accepted:
            session.flash = T("Access denied. You are not participating in that \
                problem. Please check your invitation status.")
            redirect(URL(r=request, c='choose', f='index'))

    session.problemrevision = rows[0]['revision']
    session.problemisembryo = rows[0]['isembryo']
    return problem
                
def updateProblem(id,problem):
    """ 
    Called by controller code to write the problem file and update
    the problem table. 
    """
    if problem.isAbandoned():
        db(db.problems.id==id).delete()         # nuke it.
        if request.env.web2py_runtime_gae:      # on GAE, delete invitations
                                                # manually since no cascade.
            db(db.invitations.problemid==id).delete()                                        
        return

    ## Delete any items that have passed the deletion threshold
    problem.applyDeletionVotes();

    d = checkLimits(problem)                   # Returns compressed object 
                                               # plus other fields or raises
                                               # LimitException.
    prtcpnts = len(problem.lastresponses)
    #Update the problem in the db
    db((db.problems.id==id) & 
        (db.problems.revision == session.problemrevision)).update(
        timestamp=datetime.datetime.now(),
        revision = session.problemrevision+1,
        label=problem.getLabel(100),
        participants=prtcpnts,
        solutions=d['sltns'],
        criteria=d['crtr'],
        size=d['sz'],
        gzsize=d['gzsz'],
        obj=d['gz'],
        ) 

def _addNewProblemToDb(problem):
    """ 
    Use this function, and only this function, to
    add newly created or cloned problems to the database 
    """

    d = checkLimits(problem,new=True)          # Returns compressed object 
                                               # plus other fields or raises
                                               # LimitException.
    prtcpnts = len(problem.lastresponses)
    t = datetime.datetime.now()
    session.problemrevision = 0  ## by definition of field

    id = db.problems.insert(
        timestamp=t,
        revision = session.problemrevision+1,
        label=problem.getLabel(100),
        participants=prtcpnts,
        solutions=d['sltns'],
        criteria=d['crtr'],
        size=d['sz'],
        gzsize=d['gzsz'],
        creatorid=problem.creatorid,
        created=t,
        obj=d['gz'],
          )

    ## Add and accept invitations for creator
    db.invitations.insert(email=auth.user.email, 
                          invitedby=auth.user.email,
                          problemid=id, 
                          accepted=True,
                          )
    return id                                                  
    
                       
def mergeadditions(problem,problemid=0):
    p2 = loadProblem(problemid)
    ptproblem.mergeProblem(problem,p2)
    updateProblem(problemid,p2)
    
def mergeresponses(userid,problem,problemid=0):
    p2 = loadProblem(problemid)
    ptproblem.mergeResponses(userid,problem,p2)
    updateProblem(problemid,p2)        
                               

##---------------------------------------------------------------------------
